
import java.net.Socket;
import java.io.*;
import java.util.Arrays;

final class PeerConnection implements Runnable
{

	Socket socket; // socket for the connection
	OutputStream os;	// socket's output stream
	InputStream is;	//socket's input stream
    int otherPeerID;
    int lastRequestedPiece;
    boolean isOtherPeerChoked;
    boolean amIChoked;	
	int whoConnected;

    // Constructor
	public PeerConnection(Socket socket, int whoConnected) throws Exception
	{
		this.socket = socket;
		this.whoConnected = whoConnected;
		this.isOtherPeerChoked = true;
		try
		{
			this.os = this.socket.getOutputStream();
			this.is = this.socket.getInputStream();
		}
		catch(Exception e){System.out.println(e); e.printStackTrace();}
	}

	// Implement the run() method of the Runnable interface
	public void run()
	{
		try
		{
			startConnection();
			//System.out.println("Connected to Peer: " + this.otherPeerID);
			while(true)
			{
				//if(!PeerProcess.hasEnded)
				try
				{
					processPackets();
				}
				catch(Exception f)
				{
					System.out.println(f); // print the Exception
					f.printStackTrace();
					StackTraceElement[] excep = f.getStackTrace();

					try
					{
						FileWriter fName = new FileWriter("peer_"+PeerProcess.ThisPeerId+"_exception.txt");
						fName.write(f+"\n");
						for(int j=0; j < excep.length; j++)
							fName.write(excep[j].toString()+"\n");
						fName.close();
					}
					catch(Exception g)
					{
						g.printStackTrace();
					}
				}
				Thread.yield();
			}
		}
		/*catch (Exception e)
		{
			try
			{
				this.is.close();
				this.os.close();
				this.socket.close();
				System.out.println("Connection ForceClosed");
				System.out.println(e);
				e.printStackTrace();
			}*/
			catch (Exception f)
			{
				System.out.println(f); // print the Exception
				f.printStackTrace();
				StackTraceElement[] excep = f.getStackTrace();

				try
				{
					FileWriter fName = new FileWriter("peer_"+PeerProcess.ThisPeerId+"_exception1.txt");
					fName.write(f+"\n");
					for(int j=0; j < excep.length; j++)
						fName.write(excep[j].toString()+"\n");
					fName.close();
				}
				catch(Exception g)
				{
					g.printStackTrace();
				}
			}
		//}
	}

	private void startConnection() throws Exception
	{
		int bytesRead;
		//Send sendHandshake message
		this.sendHandshakeMessage();
		
		//wait for receiveHandhshake
		byte[] receiveHandshake = new byte[32];
		bytesRead = is.read(receiveHandshake, 0, 32);
		if(bytesRead != 32)
		{
			while(bytesRead < 32)
                        {
                                bytesRead += is.read(receiveHandshake, bytesRead, 32-bytesRead);
                        }
		}
		
		PacketReader checkHandshake = new PacketReader(receiveHandshake, true);
		
		//Receives handshake and beings log & connection
		this.otherPeerID = checkHandshake.getPeerID();
		if(this.whoConnected == 0)//recevied the connection
			PeerProcess.logEntry("is connected from Peer " + this.otherPeerID);
		else
			PeerProcess.logEntry("makes a connection to Peer " + this.otherPeerID);
		//System.out.println(">>>>>>>>>>>>>From Handshake = "+this.otherPeerID);
		
		//send bitfield
		this.sendBitfieldMessage();
	
	}
	
	private void processPackets() throws Exception
	{
		this.sendAllHaveMessages();
		if(PeerProcess.hasEnded)
			return;

		//use this variable alot
		int bytesRead = 0;
		//infiniteLoop:
		//while (true)
		//{
			//wait for a new packet
			byte[] messageHeader = new byte[5];
			
			//Wait for packet to be available
			//do
			//{
				//Check if time to send choke or unchoke message
				boolean nowChoked = isOtherPeerNowChoked();
				if(nowChoked != this.isOtherPeerChoked) //if changed choked/unchoke status
				{
					this.isOtherPeerChoked = nowChoked;	//update variable
					if(this.isOtherPeerChoked)
						this.sendChokeMessage();
					else
						this.sendUnchokeMessage();
				}
				
				//Check if there is a have message to broadcast
				//this.sendAllHaveMessages();
				
			//}while(this.is.available() < 5);
			
			if(this.is.available() <5)
				return;
			
			//Packet in Queue, process it
			bytesRead = this.is.read(messageHeader, 0, messageHeader.length);
			
			//debug code if this ever prints
			if(bytesRead != 5)
			{
				while(bytesRead < 5)
	                        {
                                	bytesRead += this.is.read(messageHeader, bytesRead, 5-bytesRead);
        	                }
			}
				
			//create PacketReader
			PacketReader receivedHeader = new PacketReader(messageHeader, false);
			
			//read the new packet
			int type = receivedHeader.getMessageType();
			int length = receivedHeader.getMessageLength();
			byte[] payload;
			payload = new byte[length];
			
			bytesRead = 0;
			bytesRead = this.is.read(payload, 0, payload.length);
					
			while(bytesRead < payload.length)
			{
				bytesRead += this.is.read(payload, bytesRead, payload.length-bytesRead);
			}
			

			//for(int i = 0; i < payload.length; i++) System.out.print(payload[i]);
			//System.out.println("\nBytes Read = "+ bytesRead+" Length = "+length);
			
			//switch on packet type
			switch(type)
			{
				case 0:	//choke
					//update choke variable
					this.amIChoked = true;
					//writes into log that I got choked
					PeerProcess.logEntry("is choked by Peer " + this.otherPeerID);
					//checks to see I do not have last requested piece
					if(!PeerProcess.PeerBitmapList.peerHasPiece(PeerProcess.ThisPeerId, this.lastRequestedPiece))
						PeerProcess.PeerBitmapList.setBitmap(PeerProcess.ThisPeerId, this.lastRequestedPiece, 0);
					break;
				
				case 1:	//unchoke
					//update choke variable
					this.amIChoked = false;
					//writes into log that I got unchoked
					PeerProcess.logEntry("is unchoked by Peer " + this.otherPeerID);

					if(PeerProcess.PeerBitmapList.checkIfPieceRequested(PeerProcess.ThisPeerId, this.lastRequestedPiece))
					{
						this.sendRequestMessage(this.lastRequestedPiece);
					}
					//randomly looks for a piece in wants from the peer
					//sends REQUEST packet
					else if(PeerProcess.PeerBitmapList.checkIfInterested(this.otherPeerID))
					{
						//Choose a random piece and send request message
						int index = PeerProcess.PeerBitmapList.getPiecetoRequest(this.otherPeerID);
						this.sendRequestMessage(index);
						//this.lastRequestedPiece = index;
					}
					break;
				
				case 2:	//interested
					//writes into log that I got an interested message
					PeerProcess.logEntry("received the 'interested' message from Peer " + this.otherPeerID);
					//if not on unchoke list, add them
					if(!PeerProcess.InterestedNeighbors.contains(new Integer(this.otherPeerID)))
						PeerProcess.InterestedNeighbors.add(new Integer(this.otherPeerID));
					//if room to unchoke them, unchoke them
					//if(PeerProcess.UnChokedNeighbors.size() < PeerProcess.NumberOfPreferredNeighbors)
					//	PeerProcess.UnChokedNeighbors.add(new Integer(this.otherPeerID));
					break;
					
				case 3:	//not interested
					//writes into log that I got not interested message
					PeerProcess.logEntry("received the 'not interested' message from Peer " + this.otherPeerID);
					//remove from interested list
					if(PeerProcess.InterestedNeighbors.contains(new Integer(this.otherPeerID)))
						PeerProcess.InterestedNeighbors.remove(new Integer(this.otherPeerID));
					break;
					
				case 4:	//have
					int pieceNum = fourByteToInt(payload); 
					//writes into log that I got have message
					if(pieceNum <= PeerProcess.NumberOfPieces)
					{
						PeerProcess.logEntry("received the 'have' message from Peer " + this.otherPeerID + " for the piece " + pieceNum);
						//update data structures
						PeerProcess.PeerBitmapList.setBitmap(this.otherPeerID, pieceNum);
						//if increases a wanted counter for any peer from 0 to >0, send interested message
						//if yes -> SEND INTERESTED PACKET
						if(PeerProcess.PeerBitmapList.checkIfInterested(this.otherPeerID))
							this.sendInterestedMessage();
						else
							this.sendNotInterestedMessage();
						
						if(PeerProcess.PeerBitmapList.checkIfAllPieces(this.otherPeerID) && PeerProcess.incompletePeers.contains(new Integer(this.otherPeerID)))
							PeerProcess.incompletePeers.remove(new Integer(this.otherPeerID));
					}
					else if(pieceNum == PeerProcess.NumberOfPieces+1)
					{
						PeerProcess.PeerBitmapList.setBitmap(this.otherPeerID, -1);
						PeerProcess.incompletePeers.remove(new Integer(this.otherPeerID));
					}
					else
					{
						for(int i = 0; i < PeerProcess.indexByID.size(); i++)
							PeerProcess.PeerBitmapList.setBitmap(PeerProcess.indexByID.get(i), -1);
						PeerProcess.incompletePeers.clear();
					}
					break;
					
				case 5:	//bitfield
					//store their bitfield in memory
					PeerProcess.PeerBitmapList.setBitmap(this.otherPeerID, payload);
					//determine if he has a piece you want
					//if yes -> SEND INTERESTED PACKET
					//if no -> SEND NOT INTERESTED PACKET
					if(PeerProcess.PeerBitmapList.checkIfInterested(this.otherPeerID))
						this.sendInterestedMessage();
					else
						this.sendNotInterestedMessage();
					
					if(PeerProcess.PeerBitmapList.checkIfAllPieces(this.otherPeerID) && PeerProcess.incompletePeers.contains(new Integer(this.otherPeerID)))
						PeerProcess.incompletePeers.remove(new Integer(this.otherPeerID));
					
					break;
				
				case 6:	//request
					//checks to see if they are not choked
                                        PacketReader packet1 = new PacketReader(payload);
                                        int index21 = packet1.getIndex();   
 					
					PeerProcess.logEntry("has received a request message from peer: "+this.otherPeerID+" for piece "+index21);

					if(!this.isOtherPeerChoked)
					{
						//PacketReader packet = new PacketReader(payload);
						//int index = packet.getIndex();
						byte[] data = PeerProcess.mySplitter.returnPiece(index21);
						this.sendPieceMessage(index21, data);
						//this.lastRequestedPiece = something;
						//if yes -> ignore
						//if no -> SEND THEM THE PIECE
					}
					break;
				
				case 7:	//piece
					//update bitmaps					
					//update received count (for choking)
					this.incrementReceivedCount();
					//?sends have message to all peers?
					
					byte[] index2 = new byte[4];
					for(int i = 0; i < 4; i++)
						index2[i] = payload[i];
					
					byte[] file = new byte[payload.length-4];
					for(int i = 4; i < payload.length; i++)
						file[i-4] = payload[i];
					//System.out.println("before piece index");
					PacketReader packet = new PacketReader(index2, file);
					int pieceIndex = packet.getIndex();	//index of piece received
					//System.out.println("after piece index");
					this.addHaveMessage(pieceIndex);
					//this.sendAllHaveMessages();
					//To store the piece and update the bitmap
					int idx = pieceIndex;
					PeerProcess.mySplitter.storePiece(idx, file);

					PeerProcess.PeerBitmapList.setBitmap(PeerProcess.ThisPeerId, idx, 1);

                                        //writes into log that I got a piece
                                        int totalPieces = PeerProcess.PeerBitmapList.returnNumberOfPieces();//GET THIS FROM BITMAPS
                                        PeerProcess.logEntry("has downloaded the piece " + pieceIndex + " from Peer " + this.otherPeerID + ". Now the number of pieces it has is "+totalPieces);
                                        if(totalPieces == PeerProcess.NumberOfPieces)
										{
                                                PeerProcess.logEntry("has downloaded the complete file");
												this.addHaveMessage(PeerProcess.NumberOfPieces+1);
										}
					//Before requesting a piece, check if the current peer has all the pieces
					//If current peer has all the pieces, remove it from the incompletePeers list
					//sends not interested messages to ANY peers that no longer have wanted pieces
					//if yes -> SEND NOT INTERESTED PACKET
					//this.sendNotInterestedMessage();
					if(!PeerProcess.PeerBitmapList.checkIfAllPieces(PeerProcess.ThisPeerId))
					{
						if(PeerProcess.PeerBitmapList.checkIfInterested(this.otherPeerID))
						{
							if(idx == this.lastRequestedPiece)
							{
							//Choose a random piece and send request message
							int index = PeerProcess.PeerBitmapList.getPiecetoRequest(this.otherPeerID);
							this.sendRequestMessage(index);
							//this.lastRequestedPiece = index;
							}
						}
						else
							this.sendNotInterestedMessage();
					}
					else
					{
						this.sendNotInterestedMessage();
						if(PeerProcess.incompletePeers.contains(new Integer(PeerProcess.ThisPeerId)))
							PeerProcess.incompletePeers.remove(new Integer(PeerProcess.ThisPeerId));
					}
					
					//System.out.println("received piece# " + pieceIndex);
/*
					//writes into log that I got a piece
					int totalPieces = PeerProcess.PeerBitmapList.returnNumberOfPieces();//GET THIS FROM BITMAPS
					PeerProcess.logEntry("has downloaded the piece " + pieceIndex + " from Peer " + this.otherPeerID + ". Now the number of pieces it has is " + totalPieces);
					if(totalPieces == PeerProcess.NumberOfPieces)
						PeerProcess.logEntry("has downloaded the complete file");*/
					break;
					
				default:
					break;
			}
			
		//}// end while		
	}// end processPackets
	
	private void sendPacket(byte[] data) throws Exception
	{
		//Sending data
		this.os.write(data,0,data.length);
	    this.os.flush();
	    //System.out.println("Sending packet: " + new String(data));
	}
	
	private void sendChokeMessage() throws Exception
	{
		MessagePacket packet = new MessagePacket(0);
		this.sendPacket(packet.getBytesToSend());	
		//System.out.println("Sending Choke");
	}
	
	private void sendUnchokeMessage() throws Exception
	{
		MessagePacket packet = new MessagePacket(1);
		this.sendPacket(packet.getBytesToSend());
		//System.out.println("Sending Unchoke");
	}
	
	private void sendInterestedMessage() throws Exception
	{
		MessagePacket packet = new MessagePacket(2);
		this.sendPacket(packet.getBytesToSend());
		//System.out.println("Sending Interested");
		//PeerProcess.logEntry("Sending Interested to "+this.otherPeerID);
	}
	
	private void sendNotInterestedMessage() throws Exception
	{
		MessagePacket packet = new MessagePacket(3);
		this.sendPacket(packet.getBytesToSend());
		//System.out.println("Sending NotInterested");
		//PeerProcess.logEntry("Sending NotInterested to "+this.otherPeerID);
	}
	
	private void sendHaveMessage(int index) throws Exception
	{
		MessagePacket packet = new MessagePacket(4, index);
		this.sendPacket(packet.getBytesToSend());
		//System.out.println("Sending Have");
		//PeerProcess.logEntry("Sending Have");
	}
	
	private void sendBitfieldMessage() throws Exception
	{
		byte[] bitmap = PeerProcess.PeerBitmapList.getByteArray(PeerProcess.ThisPeerId);
		MessagePacket packet = new MessagePacket(bitmap.length, 5, bitmap);
		this.sendPacket(packet.getBytesToSend());
		//System.out.println("Sending Bitfield");
		//PeerProcess.logEntry("Sending Bitfield");
	}
	
	private void sendRequestMessage(int index) throws Exception
	{
		if(!this.amIChoked)
		{
		MessagePacket packet = new MessagePacket(6, index);
		this.sendPacket(packet.getBytesToSend());
		this.lastRequestedPiece = index;
		//System.out.println("Sending Request");
		PeerProcess.logEntry("Sending Request to "+this.otherPeerID+" for piece "+index);
		}
	}
	
	private void sendPieceMessage(int index, byte[] piece) throws Exception
	{
		MessagePacket packet = new MessagePacket(piece.length+4, 7, index, piece);
		this.sendPacket(packet.getBytesToSend());
		//System.out.println("Sending Piece");
		PeerProcess.logEntry("Sending piece "+index+" to "+this.otherPeerID);
	}
	
	private void sendHandshakeMessage() throws Exception
	{
		HandshakePacket sendHandshake = new HandshakePacket(Integer.toString(PeerProcess.ThisPeerId));
		this.sendPacket(sendHandshake.handshakeMessage());
		//System.out.println("Sending Handshake");
	}
	
	private int fourByteToInt(byte[] bytes)
	{
		String temp = "";
		for(int i=0; i<4; i++)
		{
			if(bytes[i] != 0)
			{ 
				temp = temp + (char)(bytes[i]);
			}
		}
		//convert the byte[] into an int
		return Integer.parseInt(temp);
	}
	
	private void incrementReceivedCount()
	{
		int index = PeerProcess.indexByID.indexOf(new Integer(this.otherPeerID));
		int current = PeerProcess.receivedChunks.get(index);
		PeerProcess.receivedChunks.set(index, new Integer(current+1));
	}
	
	private void addHaveMessage(int pieceIndex)
	{
		int size = PeerProcess.indexByID.size();
		for(int i=0; i<size; i++)
		{
			PeerProcess.haveMsgsToTransmit.get(i).add(new Integer(pieceIndex));
		}
	}
	
	private void sendAllHaveMessages() throws Exception
	{
		int index = PeerProcess.indexByID.indexOf(new Integer(this.otherPeerID));
		int num = PeerProcess.haveMsgsToTransmit.get(index).size();
		for(int i=0; i<num; i++)
		{
			this.sendHaveMessage(PeerProcess.haveMsgsToTransmit.get(index).get(0));
			PeerProcess.haveMsgsToTransmit.get(index).remove(0);
		}
	}
	
	private boolean isOtherPeerNowChoked()
	{
		return !(PeerProcess.UnChokedNeighbors.contains(new Integer(this.otherPeerID)) || (PeerProcess.OptimisticUnchokedNeighbor==this.otherPeerID));
	}
	
}// end class PeerConnection
